use std::{env, fs, io};
use std::borrow::Borrow;
use std::cell::{Ref, RefCell};
use std::collections::HashMap;
use std::io::{Read, stderr, stdin, stdout, Write};
use std::error::Error;
use std::fmt::format;
use std::fs::{File, OpenOptions};
use std::ops::Add;
use std::sync::{Arc, Mutex};
use regex::Regex;
use crate::lincni::plugin::PluginInfo;
use crate::lincni::types::{ErrorResponse, ErrorType};
use crate::lincni::version::{ConfigDecoder, Reconciler};
use crate::runner::Runner;
use serde::{Deserialize,Serialize};

const regex_str:&str = r"^[a-zA-Z0-9][a-zA-Z0-9_.\-]*$";

#[derive(Clone,Debug,Default)]
pub struct CmdArgs {
    pub container_id:String,
    pub net_ns:String,
    pub if_name:String,
    pub args:String,
    pub path:String,
    pub stdin_data:Vec<u8>,
}

impl CmdArgs {
    pub fn parse_args(&self) -> HashMap<String,String> {
        let mut m = HashMap::new();
        if self.args.len() == 0 {
            return m;
        }
        for i in self.args.split(";") {
            let mut arr = i.splitn(2,"=");
            if arr.clone().count() == 2 {
                let key = arr.next().unwrap();
                let value = arr.next().unwrap();
                m.insert(key.to_string(),value.to_string());
            }

        }

        m
    }
}

pub struct ReqForCmdEntry  {
    pub add:bool,
    pub del:bool,
    pub check:bool,
}

impl ReqForCmdEntry {
    pub fn to_hashmap(self) -> HashMap<String,bool> {
        let mut res = HashMap::new();
        res.insert(String::from("ADD"),self.add);
        res.insert(String::from("CHECK"),self.check);
        res.insert(String::from("DEL"),self.del);

        res
    }
}

pub struct Dispatcher
{
    pub env:HashMap<String,String>,
    pub stdin:Box<dyn Read>,
    pub stdout:Box<dyn Write>,
    pub stderr:Box<dyn Write>,
    pub conf_version_decoder:ConfigDecoder,
    pub version_reconciler:Reconciler,

    log_writer:RefCell<File>,
}

impl Default for Dispatcher {
    fn default() -> Self {
        let path = {
            if cfg!(target_os = "linux") {
                "/home/rscni.log"
            } else {
                "D:\\rscni.log"
            }
        };

        let w = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open(path)
            .unwrap();

        Dispatcher{
            env:HashMap::new(),
            stdin: Box::new(stdin()),
            stdout: Box::new(stdout()),
            stderr: Box::new(stderr()),
            conf_version_decoder: Default::default(),
            version_reconciler: Default::default(),
            log_writer: RefCell::new(w),
        }
    }
}

impl Dispatcher {
    pub fn new_from_env() -> Self {
        let mut env = HashMap::new();
        for (k,v) in env::vars() {
            env.insert(k,v);
        }

        let mut d = Dispatcher{
            env,
            ..Default::default()
        };

        // let v=  serde_json::to_value(d.env.clone()).unwrap();
        // d.log_writer.get_mut().write(v.to_string().as_bytes()).unwrap();

        d
    }

    pub fn new_from_map(data:HashMap<String,String>) -> Self {
        Dispatcher{
            env:data,
            ..Default::default()
        }
    }

    pub fn new(data:HashMap<String,String>,stdin:Box<dyn Read>,stdout:Box<dyn Write>,stderr:Box<dyn Write>) -> Self {
        Dispatcher{
            env:data,
            stdin,
            stdout,
            stderr,
            ..Default::default()
        }
    }

    pub fn plugin_main_with_err<R>(
        &mut self,
        mut runner:R,
        version:PluginInfo,
        about:&str,
    ) -> Result<(),ErrorResponse>
        where
            R:Runner
    {
        let res = self.get_cmd_args_from_env();
        if let Err(e) = res.borrow() {
            return match e.code {
                ErrorType::ErrInvalidEnvironmentVariables => {
                    if (self.env.get("CNI_COMMAND").unwrap().len() > 0) && about.len() > 0 {
                        let str = format!("{}\nCNI protocol versions supported: {}\n",
                                          about, version.support_versions_string(", "),
                        );

                        let _ = self.stderr.write(str.as_bytes());
                        Ok(())
                    } else {
                        Err(e.clone())
                    }
                }
                _ => {
                    Err(e.clone())
                }
            }
        }
        let (cmd,args) = res.unwrap();
        self.log_writer.get_mut().write(format!("cmd : {}\n",cmd).as_bytes());
        self.log_writer.get_mut().write(format!("args : {:?}\n",args).as_bytes());
        self.log_writer.get_mut().write(format!("stdin : {}\n",String::from_utf8(args.stdin_data.clone()).unwrap()).as_bytes());
        if cmd != "VERSION" {
            self.validate_config(&args.stdin_data)?;
            self.validate_container_id(&args.container_id)?;
        }

        return match cmd.as_str() {
            "ADD" => {
                self.check_version(&args,version)?;
                let r = runner.add(&args,self.log_writer.get_mut());
                if let Err(e) = r {
                    self.log_writer.get_mut().write(e.error().as_bytes()).unwrap();
                }
                Ok(())
            },
            "DEL" => {
                self.check_version(&args,version)?;
                let r =  runner.del(&args,self.log_writer.get_mut());
                if let Err(e) = r {
                    self.log_writer.get_mut().write(e.error().as_bytes()).unwrap();
                }
                Ok(())
            },
            "CHECK" => {
                let c = self.conf_version_decoder.decode(args.stdin_data.clone());
                if let Err(e) = c.borrow() {
                    return Err(ErrorResponse::new(
                        ErrorType::ErrIncompatibleCNIVersion,
                        "config version does not allow CHECK".to_string(),
                        "".to_string()));
                }

                let c = c.unwrap();
                match self.conf_version_decoder.greater_than_or_equal_to(c.as_str(),"0.4.0") {
                    Ok(gtet) => {
                        if !gtet {
                            return Err(ErrorResponse::new(
                                ErrorType::ErrIncompatibleCNIVersion,
                                "config version does not allow CHECK".to_string(),
                                "".to_string()));
                        }
                    }
                    Err(e) => { return Err(ErrorResponse::new(
                        ErrorType::ErrDecodingFailure,
                        e.to_string(),
                        "".to_string()));}
                }

                for i in version.support_versions() {
                    match self.conf_version_decoder.greater_than_or_equal_to(i.as_str(),c.as_str()) {
                        Ok(gtet) => {
                            if gtet {
                                self.check_version(&args,version)?;
                                runner.check(&args,self.log_writer.get_mut())?;
                                return Ok(());
                            }
                        }
                        Err(e) => { return Err(ErrorResponse::new(
                            ErrorType::ErrDecodingFailure,
                            e.to_string(),
                            "".to_string()));}
                    }
                }
                return Err(ErrorResponse::new(
                    ErrorType::ErrIncompatibleCNIVersion,
                    "plugin version does not allow CHECK".to_string(),
                    "".to_string()));
            },
            "VERSION" => {
                if let Err(e) = self.stdout.write(serde_json::to_string_pretty(&version).unwrap().as_bytes()) {
                     Err(ErrorResponse::new(
                        ErrorType::ErrIOFailure,
                        e.to_string(),
                        "".to_string()))
                } else {
                    Ok(())
                }
            },
            _ => {
                Err(ErrorResponse::new(
                    ErrorType::ErrInvalidEnvironmentVariables,
                    format!("unknown CNI_COMMAND: {}", cmd),
                    "".to_string()))
            }
        }

    }


    fn get_cmd_args_from_env(&mut self) -> Result<(String,CmdArgs),ErrorResponse> {
        let (mut cmd,
            mut cont_id,
            mut netns,
            mut if_name,
            mut args,
            mut path)
            = (Arc::new(Mutex::new(String::new())),
               Arc::new(Mutex::new(String::new())),
               Arc::new(Mutex::new(String::new())),
               Arc::new(Mutex::new(String::new())),
               Arc::new(Mutex::new(String::new())),
               Arc::new(Mutex::new(String::new())),);

        struct Tmp<'a> {
            name:&'a str,
            val:Arc<Mutex<String>>,
            req_for_cmd:HashMap<String,bool>,
        }

        let mut vars:Vec<Tmp> = vec![
            Tmp{name:"CNI_COMMAND",val:cmd.clone(),req_for_cmd:ReqForCmdEntry{add:true,del:true,check:true}.to_hashmap()},
            Tmp{name:"CNI_CONTAINERID",val:cont_id.clone(),req_for_cmd:ReqForCmdEntry{add:true,del:true,check:true}.to_hashmap()},
            Tmp{name:"CNI_NETNS",val:netns.clone(),req_for_cmd:ReqForCmdEntry{add:true,del:true,check:false}.to_hashmap()},
            Tmp{name:"CNI_IFNAME",val:if_name.clone(),req_for_cmd:ReqForCmdEntry{add:true,del:true,check:true}.to_hashmap()},
            Tmp{name:"CNI_ARGS",val:args.clone(),req_for_cmd:ReqForCmdEntry{add:false,del:false,check:false}.to_hashmap()},
            Tmp{name:"CNI_PATH",val:path.clone(),req_for_cmd:ReqForCmdEntry{add:true,del:true,check:true}.to_hashmap()},
        ];

        let mut std_data = String::new();
        match self.stdin.read_to_string(&mut std_data) { _ => {} }

        let mut args_missing = vec![];

        for mut i in vars.iter() {
            let iname =  self.env.get(i.name).unwrap().clone();
            if iname.is_empty() {
                if let Some(cmd_str) = i.req_for_cmd.get(cmd.lock().unwrap().as_str()) {
                    if (cmd_str.clone()) || (i.name == "CNI_COMMAND") {
                        args_missing.push(i.name.to_string());
                    }
                }
            }
            *i.val.lock().unwrap() = iname;
        }

        if args_missing.len() > 0 {
            let str = {
                let mut data = String::new();
                for i in args_missing {
                    data = data.add(i.as_str());
                    data.push(',');
                }
                data.pop();

                data
            };
            return Err(ErrorResponse::new(
                ErrorType::ErrInvalidEnvironmentVariables,
                format!("required env variables {} missing", str),
                "".to_string()
            ));
        }

        let cmd_args = CmdArgs {
            container_id: cont_id.lock().unwrap().clone(),
            net_ns:netns.lock().unwrap().clone(),
            if_name:if_name.lock().unwrap().clone(),
            args:args.lock().unwrap().clone(),
            path:path.lock().unwrap().clone(),
            stdin_data: std_data.as_bytes().to_vec()
        };

        let new_cmd = cmd.lock().unwrap().clone();
        Ok((new_cmd,cmd_args))
    }

    fn validate_config(&self,data:&Vec<u8>) -> Result<(),ErrorResponse> {
        #[derive(Serialize,Deserialize)]
        struct conf {
            name:String,
        }

        let res = serde_json::from_slice(data.as_slice());
        match res {
            Err(e) => {
                return Err(ErrorResponse::new(
                    ErrorType::ErrDecodingFailure,
                         format!("error unmarshall network config: {}", e.to_string()),
                          "".to_string()));
            },
            _ => {
            },
        }

        let c:conf = res.unwrap();
        if c.name.is_empty() {
            return Err(ErrorResponse::new(
                ErrorType::ErrInvalidNetworkConfig,
                format!("missing network name"),
                "".to_string()));
        }
        if let Err(e) = self.validate_network_name(&c.name) {
            return Err(e);
        }

        Ok(())
    }

    fn validate_network_name(&self,name:&String) -> Result<(),ErrorResponse> {
        if name.is_empty() {
            return Err(ErrorResponse::new(
                ErrorType::ErrInvalidNetworkConfig,
                format!("missing network name:"),
                "".to_string()));
        }
        if !Regex::new(regex_str).unwrap().is_match(name.as_str()) {
            return Err(ErrorResponse::new(
                ErrorType::ErrInvalidNetworkConfig,
                format!("invalid characters found in network name"),
                name.clone()));
        }

        Ok(())
    }

    fn validate_container_id(&self,id:&String) -> Result<(),ErrorResponse> {
        if id.len() == 0 {
            return Err(ErrorResponse::new(
                ErrorType::ErrUnknownContainer,
                format!("missing containerID"),
                "".to_string()));
        }
        if !Regex::new(regex_str).unwrap().is_match(id.as_str()) {
            return Err(ErrorResponse::new(
                ErrorType::ErrInvalidEnvironmentVariables,
                format!("invalid characters in containerID"),
                id.clone()));
        }

        Ok(())
    }

    fn check_version(&self,args:&CmdArgs,vi:PluginInfo) -> Result<(),ErrorResponse> {
        let res=  self.conf_version_decoder.decode(args.stdin_data.clone());
        if let Err(e) = res {
            return Err(ErrorResponse::new(
                ErrorType::ErrDecodingFailure,
                e.to_string(),
                "".to_string()));
        }
        if let Err(e) = self.version_reconciler.check(res.unwrap(),vi) {
            return Err(ErrorResponse::new(
                ErrorType::ErrIncompatibleCNIVersion,
                "incompatible CNI versions".to_string(),
                e.details()))
        }

        Ok(())
    }
}

#[cfg(test)]
mod test {
    use crate::CmdArgs;

    #[test]
    fn test () {
        let arg = "IgnoreUnknown=1;K8S_POD_NAMESPACE=default;K8S_POD_NAME=busybox-967c8b94b-b7zsm;K8S_POD_INFRA_CONTAINER_ID=c11f427326ff978be10ec75229c3965cf29d03da3f8beabc7b84aea29b7ef285";

        let a = CmdArgs{
            container_id: "".to_string(),
            net_ns: "".to_string(),
            if_name: "".to_string(),
            args:arg.to_string(),
            path: "".to_string(),
            stdin_data: vec![]
        };
        let m = a.parse_args();
        println!("{:?}",m);
    }
}
